package timbap.visualizer.api.plugin.poscloud;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import timbap.visualizer.api.context.PluginContext;
import timbap.visualizer.api.model.PlaylistEntry;
import timbap.visualizer.api.plugin.poscloud.cache.DefaultPosCloudCacheImpl;
import timbap.visualizer.api.plugin.poscloud.cache.PosCloudCache;
import timbap.visualizer.api.plugin.poscloud.cache.AbstractPosCloudMapXmlAdapter;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;

/**
 * The <code>AbstractCacheablePosCloudPlugin</code> is the common base
 * implementation for all positioning cloud plugins that can cache
 * their positioning cloud items for better start-up performance.
 *
 * It uses a <code>PosCloudCache</code> that stores all the relevant
 * data in serializable form. The implementation is independent from
 * the type of positioning cloud element that is used.
 *
 * @author  Andreas Pabst
 * @see     timbap.visualizer.api.plugin.poscloud.CacheablePosCloudPlugin
 * @see     timbap.visualizer.api.plugin.poscloud.AbstractPosCloudPlugin
 * @see     timbap.visualizer.api.plugin.poscloud.cache.PosCloudCache
 */
public abstract class AbstractCacheablePosCloudPlugin<E extends Comparable<? super E>>
                        extends AbstractPosCloudPlugin<E> implements CacheablePosCloudPlugin<E> {

    private static final Log LOG = LogFactory.getLog(AbstractCacheablePosCloudPlugin.class);
    private static final Class[] CLASSES_TO_BIND = {DefaultPosCloudCacheImpl.class};

    private static JAXBContext jaxbContext;

    private static JAXBContext getJaxbContext() throws JAXBException {
        if (jaxbContext == null) {
            jaxbContext = JAXBContext.newInstance(CLASSES_TO_BIND);
        }
        return jaxbContext;
    }

    @Override
    public void init(PluginContext pPluginContext) {
        super.init(pPluginContext);
        LOG.info("Loading cache of plugin " + this + " ...");
        PosCloudCache<E> tFullPosCloudCache = new DefaultPosCloudCacheImpl<E>();
        try {
            tFullPosCloudCache = load();
            Map<PlaylistEntry, E> tUsedPosCloudCache = new HashMap<PlaylistEntry, E>();
            for (PlaylistEntry tPlaylistEntry: pPluginContext.getPlaylistManager().getPlaylist()) {
                if (tFullPosCloudCache.getMap().containsKey(tPlaylistEntry)) {
                    E tCachedPosCloudElement = tFullPosCloudCache.getMap().get(tPlaylistEntry);
                    if (isCachedPosCloudElementValid(tPlaylistEntry, tCachedPosCloudElement)) {
                        if (LOG.isDebugEnabled()) {
                            LOG.debug("Using cached posCloudElement " + tCachedPosCloudElement + " for playlist entry " + tPlaylistEntry);
                        }
                        tUsedPosCloudCache.put(tPlaylistEntry, tCachedPosCloudElement);
                    } else {
                        LOG.info("Validation of cached posCloudElement for playlist entry " + tPlaylistEntry + " failed.");
                        tFullPosCloudCache.getMap().remove(tPlaylistEntry);
                    }
                }
            }
            addToPosCloudElementMap(tUsedPosCloudCache);
            LOG.info("Successfully loaded elements for " + tUsedPosCloudCache.size() + " playlist entries " 
                    + " from cache of plugin " + this + " ...");
        } catch (Exception e) {
            LOG.warn("Loading cache of plugin " + this + " failed: " + e.getMessage());
        }

        //TODO move this back to the ArtworkHuePosCloudPlugin
        setElementDisplayCentered(false);
        setElementWidthFixed(true);

        LOG.info("Saving cache of plugin " + this + " ...");
        try {
            Map<PlaylistEntry, E> tJoinedPosCloudCache = new HashMap<PlaylistEntry, E>();
            tJoinedPosCloudCache.putAll(tFullPosCloudCache.getMap());
            tJoinedPosCloudCache.putAll(getPosCloudElementMap());
            save(new DefaultPosCloudCacheImpl<E>(tJoinedPosCloudCache));
            LOG.info("Successfully saved " + tJoinedPosCloudCache.size() + " items to cache of plugin " + this + " ...");
        } catch (Exception e) {
            LOG.warn("Saving cache of plugin " + this + "failed." + e.getMessage());
            e.printStackTrace();
        }
    }

    @Override
    public void initPosCloudElementMap() {
        Map<PlaylistEntry, E> tPosCloudElementMap = new HashMap<PlaylistEntry, E>();
        for (PlaylistEntry tPlaylistEntry : getContext().getPlaylistManager().getPlaylist()) {
            E tPosCloudItem = findPosCloudElement(tPlaylistEntry);
            if (tPosCloudItem == null) {
                tPosCloudItem = createPosCloudElement(tPlaylistEntry);
            } 
            tPosCloudElementMap.put(tPlaylistEntry, tPosCloudItem);
        }
        addToPosCloudElementMap(tPosCloudElementMap);
    }


    public void save(PosCloudCache<E> pPosCloudCache) throws IOException, JAXBException {
        OutputStream tOut = new BufferedOutputStream(new FileOutputStream(getCacheFile()));
        Marshaller tMarshaller = getJaxbContext().createMarshaller();
        tMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        tMarshaller.setAdapter(AbstractPosCloudMapXmlAdapter.class, getPosCloudMapXmlAdapter());
        tMarshaller.marshal(pPosCloudCache, tOut);
        tOut.flush();
        tOut.close();
    }

    public PosCloudCache<E> load() throws IOException, JAXBException {
        InputStream tIn = new FileInputStream(getCacheFile());
        Unmarshaller tUnmarshaller = getJaxbContext().createUnmarshaller();
        tUnmarshaller.setAdapter(AbstractPosCloudMapXmlAdapter.class,
                                 getPosCloudMapXmlAdapter());
        PosCloudCache<E> tPosCloudCache =
                (PosCloudCache<E>) tUnmarshaller.unmarshal(tIn);
        tIn.close();
        return tPosCloudCache;
    }

}